// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('cr.ui', function() { /** @const */ var Command = cr.ui.Command; /** * Creates a new menu item element. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {HTMLDivElement} */ var MenuItem = cr.ui.define('div'); /** * Creates a new menu separator element. * @return {cr.ui.MenuItem} The new separator element. */ MenuItem.createSeparator = function() { var el = cr.doc.createElement('hr'); MenuItem.decorate(el); return el; }; MenuItem.prototype = { __proto__: HTMLButtonElement.prototype, /** * Initializes the menu item. */ decorate: function() { var commandId; if ((commandId = this.getAttribute('command'))) this.command = commandId; this.addEventListener('mouseup', this.handleMouseUp_); // Adding the 'custom-appearance' class prevents widgets.css from changing // the appearance of this element. this.classList.add('custom-appearance'); this.setAttribute('role', 'menuitem'); var iconUrl; if ((iconUrl = this.getAttribute('icon'))) this.iconUrl = iconUrl; }, /** * The command associated with this menu item. If this is set to a string * of the form "#element-id" then the element is looked up in the document * of the command. * @type {cr.ui.Command} */ command_: null, get command() { return this.command_; }, set command(command) { if (this.command_) { this.command_.removeEventListener('labelChange', this); this.command_.removeEventListener('disabledChange', this); this.command_.removeEventListener('hiddenChange', this); this.command_.removeEventListener('checkedChange', this); } if (typeof command == 'string' && command[0] == '#') { command = this.ownerDocument.getElementById(command.slice(1)); cr.ui.decorate(command, Command); } this.command_ = command; if (command) { if (command.id) this.setAttribute('command', '#' + command.id); this.label = command.label; this.disabled = command.disabled; this.hidden = command.hidden; this.command_.addEventListener('labelChange', this); this.command_.addEventListener('disabledChange', this); this.command_.addEventListener('hiddenChange', this); this.command_.addEventListener('checkedChange', this); } this.updateShortcut_(); }, /** * The text label. * @type {string} */ get label() { return this.textContent; }, set label(label) { this.textContent = label; }, /** * Menu icon. * @type {string} */ get iconUrl() { return this.style.backgroundImage; }, set iconUrl(url) { this.style.backgroundImage = 'url(' + url + ')'; }, /** * @return {boolean} Whether the menu item is a separator. */ isSeparator: function() { return this.tagName == 'HR'; }, /** * Updates shortcut text according to associated command. If command has * multiple shortcuts, only first one is displayed. */ updateShortcut_: function() { this.removeAttribute('shortcutText'); if (!(this.command_ && this.command_.shortcut)) return; var shortcuts = this.command_.shortcut.split(/\s+/); if (shortcuts.length == 0) return; var shortcut = shortcuts[0]; var mods = {}; var ident = ''; shortcut.split('-').forEach(function(part) { var partUc = part.toUpperCase(); switch (partUc) { case 'CTRL': case 'ALT': case 'SHIFT': case 'META': mods[partUc] = true; break; default: console.assert(!ident, 'Shortcut has two non-modifier keys'); ident = part; } }); var shortcutText = ''; // TODO(zvorygin): if more cornercases appear - optimize following // code. Currently 'Enter' keystroke is passed as 'Enter', and 'Space' // is passed as 'U+0020' if (ident == 'U+0020') ident = 'Space'; ['CTRL', 'ALT', 'SHIFT', 'META'].forEach(function(mod) { if (mods[mod]) shortcutText += loadTimeData.getString('SHORTCUT_' + mod) + '+'; }); if (ident.indexOf('U+') != 0) { shortcutText += loadTimeData.getString('SHORTCUT_' + ident.toUpperCase()); } else { shortcutText += String.fromCharCode(parseInt(ident.substring(2), 16)); } this.setAttribute('shortcutText', shortcutText); }, /** * Handles mouseup events. This dispatches an activate event; if there is an * associated command, that command is executed. * @param {Event} e The mouseup event object. * @private */ handleMouseUp_: function(e) { if (!this.disabled && !this.isSeparator() && this.selected) { // Store |contextElement| since it'll be removed by {Menu} on handling // 'activate' event. var contextElement = this.parentNode.contextElement; var activationEvent = cr.doc.createEvent('Event'); activationEvent.initEvent('activate', true, true); activationEvent.originalEvent = e; // Dispatch command event followed by executing the command object. if (this.dispatchEvent(activationEvent)) { var command = this.command; if (command) { command.execute(contextElement); cr.ui.swallowDoubleClick(e); } } } }, /** * Updates command according to the node on which this menu was invoked. * @param {Node=} opt_node Node on which menu was opened. */ updateCommand: function(opt_node) { if (this.command_) { this.command_.canExecuteChange(opt_node); } }, /** * Handles changes to the associated command. * @param {Event} e The event object. */ handleEvent: function(e) { switch (e.type) { case 'disabledChange': this.disabled = this.command.disabled; break; case 'hiddenChange': this.hidden = this.command.hidden; break; case 'labelChange': this.label = this.command.label; break; case 'checkedChange': this.checked = this.command.checked; break; } } }; /** * Whether the menu item is disabled or not. * @type {boolean} */ cr.defineProperty(MenuItem, 'disabled', cr.PropertyKind.BOOL_ATTR); /** * Whether the menu item is hidden or not. * @type {boolean} */ cr.defineProperty(MenuItem, 'hidden', cr.PropertyKind.BOOL_ATTR); /** * Whether the menu item is selected or not. * @type {boolean} */ cr.defineProperty(MenuItem, 'selected', cr.PropertyKind.BOOL_ATTR); /** * Whether the menu item is checked or not. * @type {boolean} */ cr.defineProperty(MenuItem, 'checked', cr.PropertyKind.BOOL_ATTR); // Export return { MenuItem: MenuItem }; });